/*
 * read_write_mayWrite_lock.c
 *
 *  Created on: May 23, 2012
 *      Author: gb
 */

#include <stdbool.h>
#include <stdlib.h>
#include <pthread.h>
#include <assert.h>
#include "hw3_helpers.h"
#include "read_write_mayWrite_lock.h"

typedef enum { READ = 0, MAY_WRITE, WRITE } LOCK_TYPE;

typedef enum { DEFAULT, MAY_WRITER_EXIST, WRITER_EXIST } FLAG;

typedef struct node_t {
	struct node_t* next;
	pthread_cond_t node_cond;
	LOCK_TYPE lt;
} *Node;

typedef struct queue_t {
	int num_of_element;
	Node head; // this is the next element to pop.
	Node last; // this will be the location where to place the new inserted node
} *Queue;

struct rwmw_lock_t {
	int number_of_r;
	int upgrade_issued;
	pthread_t writer_id;
	pthread_t mWriter_id;
	FLAG flag;
	pthread_mutex_t global_lock;
	pthread_cond_t upgrade_condition;
	Queue queue;
};

/* ========== Queue manipulation functions ========== */

void queue_destroy_node(Node node) {
	if (node) {
		pthread_cond_destroy(&node->node_cond);
		free(node);
	}
}

Queue queue_init(){
	Queue new_queue;
	new_queue = malloc(sizeof(struct queue_t));
	if (!new_queue) {
		return NULL;
	}
	new_queue->num_of_element =0;
	new_queue->head = NULL;
	new_queue->last = NULL;
	return new_queue;
}

void queue_destroy(Queue queue) {
	if (!queue) {
		return;
	}
	Node it = queue->head;
	Node temp_node;
	while (it) {
		temp_node = it;
		it = it->next;
		queue_destroy_node(temp_node);
	}
	free(queue);
}

void queue_enqueue(Queue queue, Node new_node) {
	if ((!queue) || (!new_node)) {
		return;
	}
	if (queue->last == NULL) {
		assert(queue->head == NULL);
		queue->last = new_node;
		queue->head = new_node;
	} else {
		queue->last->next = new_node;
		queue->last = new_node;
	}
	queue->num_of_element++;
}

Node queue_top(Queue queue) {
	if (queue) {
		return queue->head;
	}
	return NULL;
}

void queue_pop(Queue queue) {
	if (queue==NULL) return;
	if (!queue->num_of_element) return;
	Node tmp=queue->head;
	queue->num_of_element--;
	if (!queue->num_of_element) {
		queue->head=NULL;
		queue->last=NULL;
	} else {
		queue->head = queue->head->next;
	}
	queue_destroy_node(tmp);
}

bool queue_is_empty(Queue queue) {
	if (queue==NULL) return true;
	if (queue->num_of_element) return false;
	return true;
}

Node queue_create_node(LOCK_TYPE lt) {
	Node new_node = malloc(sizeof(struct node_t));
	if (new_node == NULL) {
		assert(0);
		return NULL;
	}
	new_node->next = NULL;
	new_node->lt = lt;
	pthread_cond_init(&new_node->node_cond, NULL);
	return new_node;
}

/* ========== Helper function ========== */

void wakeup_queue(RWMW_Lock l) {
	//wake no one if the queue is empty or an upgrade was issued
	if (!queue_is_empty(l->queue) && !l->upgrade_issued) {
		Node head = queue_top(l->queue);
		//if first thread in the queue is a writer thread
		if (head->lt == WRITE) {
			//if the writer's condition are met wake up the writer
			if (!l->number_of_r && l->flag == DEFAULT) {
				pthread_cond_signal(&head->node_cond);
				queue_pop(l->queue);
			}
			//there's already a mw in the lock, wakeup only the first readers
		} else if (l->flag == MAY_WRITER_EXIST) {
			while (head && head->lt == READ) {
				pthread_cond_signal(&head->node_cond);
				queue_pop(l->queue);
				head = queue_top(l->queue);
			}
			//there are no mw in the lock, wakeup readers and one mw
		} else if (l->flag == DEFAULT) {
			bool mw_wokeup = false;
			while (head && ((head->lt == READ) || ((head->lt == MAY_WRITE) && (!mw_wokeup)))) {
				if (head->lt == MAY_WRITE)
					mw_wokeup = true;
				pthread_cond_signal(&head->node_cond);
				queue_pop(l->queue);
				head = queue_top(l->queue);
			}
		} else {
			assert(0); //can't be here with flag=WRITER_EXISTS
		}
	}
	return;
}

/* ========== Interface functions ========== */

RWMW_Lock init_lock() {
	int init_val;
	RWMW_Lock new_lock;
	new_lock = malloc(sizeof(struct rwmw_lock_t));
	if(!new_lock) {
		return NULL;
	}
	new_lock->queue = queue_init();
	if (!new_lock->queue) {
		free(new_lock);
		return NULL;
	}
	new_lock->number_of_r = 0;
	new_lock->upgrade_issued = 0;
	new_lock->flag = DEFAULT;
	init_val = pthread_cond_init(&new_lock->upgrade_condition, NULL);
	assert(init_val == 0);
	ec_mutex_init(&new_lock->global_lock);
	return new_lock;
}

void destroy_lock(RWMW_Lock l) {
	int ret_val1, ret_val2;
	if (!l) {
		return;
	}
	ret_val1 = pthread_mutex_destroy(&l->global_lock);
	ret_val2 =pthread_cond_destroy(&l->upgrade_condition);
	assert((ret_val1 == 0) && (ret_val2 == 0));
	queue_destroy(l->queue);
	free(l);
}

void get_read_lock(RWMW_Lock l) {
	/*
	 *  as we know, malloc can sleep, thats why we allocate
	 *  memory for a new node even before we acquire a lock
	 *  and do anything with the queue
	 */
	Node new_reader = queue_create_node(READ);
	if (!new_reader) {
		// Memory allocation failed!
		assert(0);
		return;
	}
	pthread_mutex_lock(&(l->global_lock));
	if ((!queue_is_empty(l->queue)) || (l->flag == WRITER_EXIST) || (l->upgrade_issued == 1)) {
		queue_enqueue(l->queue, new_reader);
		do {
			pthread_cond_wait(&new_reader->node_cond, &l->global_lock);
		} while ((l->flag == WRITER_EXIST) || (l->upgrade_issued == 1));
	} else {
		// node didn't go in the queue and needs to be destroyed
		queue_destroy_node(new_reader);
	}
	l->number_of_r++;
	pthread_mutex_unlock(&l->global_lock);
}

void get_may_write_lock(RWMW_Lock l) {
	/*
	 *  for the same reason stated above, we first allocate memory for a new node.
	 */
	Node new_mWriter = queue_create_node(MAY_WRITE);
	if (!new_mWriter) {
		// Memory allocation failed!
		assert(0);
		return;
	}
	pthread_mutex_lock(&(l->global_lock));
	if ((!queue_is_empty(l->queue)) || (l->flag != DEFAULT)) {
		queue_enqueue(l->queue, new_mWriter);
		do {
			pthread_cond_wait(&new_mWriter->node_cond, &l->global_lock);
		} while (l->flag != DEFAULT);
	} else {
		// node didn't go in the queue and needs to be destroyed
		queue_destroy_node(new_mWriter);
	}
	l->flag = MAY_WRITER_EXIST;
	l->mWriter_id = pthread_self();
	pthread_mutex_unlock(&(l->global_lock));
}
void get_write_lock(RWMW_Lock l) {
	/*
	 *  for the same reason stated above, we first allocate memory for a new node.
	 */
	Node new_writer = queue_create_node(WRITE);
	if (!new_writer) {
		// Memory allocation failed!
		assert(0);
		return;
	}

	pthread_mutex_lock(&(l->global_lock));
	if (!queue_is_empty(l->queue) || (l->number_of_r != 0) || (l->flag
			!= DEFAULT)) {
		queue_enqueue(l->queue, new_writer);
		do {
			pthread_cond_wait(&(new_writer->node_cond), &l->global_lock);
		} while ((l->number_of_r != 0) || (l->flag != DEFAULT));
	} else {
		// node didn't go in the queue and needs to be destroyed
		queue_destroy_node(new_writer);
	}
	l->flag = WRITER_EXIST;
	l->writer_id = pthread_self();
	pthread_mutex_unlock(&(l->global_lock));
}
void upgrade_may_write_lock(RWMW_Lock l) {
	pthread_mutex_lock(&(l->global_lock));
	if ((l->flag != MAY_WRITER_EXIST) || (!pthread_equal(pthread_self(),l->mWriter_id))) {
		// there are no may_writers, or this is not the may_writer!
		assert(false); // for debugging
		return; // for practice
	}
	l->upgrade_issued = 1;
	while (l->number_of_r > 0) {
		/*
		 * we make sure that when waking up there are no more readers active
		 * thats why we can use "if" instead of "while"
		 */
		pthread_cond_wait(&(l->upgrade_condition), &l->global_lock);
	}
	assert(l->number_of_r == 0);
	l->flag = WRITER_EXIST;
	l->writer_id = pthread_self();
	l->upgrade_issued = 0;
	pthread_mutex_unlock(&(l->global_lock));
}
void release_shared_lock(RWMW_Lock l) {
	pthread_mutex_lock(&l->global_lock);
	if (l->flag == WRITER_EXIST) {
		// if there is a writer, it must be the only thread holding this lock
		// can't release a writers lock with this...
		return;
	}
	if ((l->flag == MAY_WRITER_EXIST) && pthread_equal(pthread_self(), l->mWriter_id)) {
		assert(l->upgrade_issued == 0);
		l->flag = DEFAULT;
	} else {
		// this is a reader
		l->number_of_r--;
		if ((l->number_of_r == 0) && (l->upgrade_issued)) {
			pthread_cond_signal(&(l->upgrade_condition));
		}
	}
	wakeup_queue(l);
	pthread_mutex_unlock(&l->global_lock);
}
void release_exclusive_lock(RWMW_Lock l) {
	pthread_mutex_lock(&l->global_lock);
	if (l->flag != WRITER_EXIST) {
		assert(0);
		return;
	}
	assert(pthread_equal(pthread_self(), l->writer_id));
	assert(l->number_of_r == 0);
	l->flag = DEFAULT;
	wakeup_queue(l);
	pthread_mutex_unlock(&l->global_lock);
}
